package service

import (
	"context"

	"happy-sudoku/graphql/models"
	"happy-sudoku/internal/codec"
	"happy-sudoku/internal/manager"
	"happy-sudoku/internal/model"
	"happy-sudoku/internal/repository"
	"happy-sudoku/internal/sudoku"

	"github.com/pkg/errors"
	"golang.org/x/sync/errgroup"
)

// UpdateSettingInput is arg for UpdateSetting
type UpdateSettingInput struct {
	RoomID      int
	PlayerID    int
	RoomSetting model.RoomSetting
}

// !! TODO: Error Handling

// IRoomService defines the behaviour of RoomService
type IRoomService interface {
	// GetAllRooms will return all of rooms
	GetAllRooms(ctx context.Context) ([]*model.Room, error)
	// GetOneRoom will return one specific room
	GetOneRoom(ctx context.Context, roomID int) (*model.Room, error)
	// GetRooms will return a list of room
	GetRooms(ctx context.Context, roomIds ...int) ([]*model.Room, error)
	// Create Room will create room, and set host
	CreateRoom(ctx context.Context, hostID int) (*model.Room, error)
	// JoinRoom make a player joins the room
	JoinRoom(ctx context.Context, roomID int, playerID int) (*model.Room, error)
	// LeaveRoom make a player leave the room
	LeaveRoom(ctx context.Context, playerID int) error
	// UpdateSetting update Room Details
	UpdateSetting(ctx context.Context, input UpdateSettingInput) (*model.Room, error)
	// Room Operation on game management
	// StartGame start game
	StartGame(ctx context.Context, playerID int) error
	// PushAnswer commit answer
	PushAnswer(ctx context.Context, playerID int, answer sudoku.GridType) (*sudoku.AnswerRes, error)
	// TogglePlayerReadyOrWait change player status to ready or wait
	TogglePlayerReadyOrWait(ctx context.Context, playerID int, ready bool) (bool, error)
	// SubscribePlayerState sub playerState
	SubscribePlayerState(ctx context.Context, playerID int) (<-chan *model.PlayerState, error)
	// SubscribeGameTime sub gametTime
	SubscribeGameTime(ctx context.Context, playerID int) (<-chan model.TimeDuration, error)
	// SubscribeRoom sub room
	SubscribeRoom(ctx context.Context, playerID int) (<-chan *model.Room, error)
	// SubscribeRoomListUpdate subscribe when room added or deleted in roomList
	SubscribeRoomListUpdate(ctx context.Context, playerID int) (<-chan *models.RoomListUpdatedRes, error)
}

// RoomServiceCfg build RoomService
type RoomServiceCfg struct {
	RoomRepo    repository.IRoomRepo
	PuzzleMgr   manager.IPuzzleManager
	PlayerMgr   manager.IPlayerManager
	RoomSyncMgr manager.IRoomSyncManager
	RoomMgr     manager.IRoomManager
}

// RoomService implements IRoomService
type RoomService struct {
	// access to repo of room
	roomRepo    repository.IRoomRepo
	puzzleMgr   manager.IPuzzleManager
	playerMgr   manager.IPlayerManager
	roomSyncMgr manager.IRoomSyncManager
	roomMgr     manager.IRoomManager
}

const (
	roomServiceErrHeader = "RoomService."
)

// NewRoomService creates RoomService
func NewRoomService(cfg RoomServiceCfg) *RoomService {
	return &RoomService{
		roomRepo:    cfg.RoomRepo,
		puzzleMgr:   cfg.PuzzleMgr,
		playerMgr:   cfg.PlayerMgr,
		roomSyncMgr: cfg.RoomSyncMgr,
		roomMgr:     cfg.RoomMgr,
	}
}

// GetAllRooms will return all of rooms
func (r *RoomService) GetAllRooms(ctx context.Context) ([]*model.Room, error) {
	const errHeader = roomServiceErrHeader + "GetAllRooms"
	rooms, err := r.roomRepo.GetAll()
	return rooms, errors.Wrap(err, errHeader)
}

// GetOneRoom will return one specific room
func (r *RoomService) GetOneRoom(ctx context.Context, roomID int) (*model.Room, error) {
	const errHeader = roomServiceErrHeader + "GetOneRoom"
	room, err := r.roomRepo.GetOne(roomID)
	return room, errors.Wrap(err, errHeader)
}

// GetRooms will return a list of room
func (r *RoomService) GetRooms(ctx context.Context, roomIds ...int) ([]*model.Room, error) {
	const errHeader = roomServiceErrHeader + "GetRooms"
	rooms, err := r.roomRepo.GetList(roomIds)
	return rooms, errors.Wrap(err, errHeader)
}

// CreateRoom will create room with default setting, and set host
func (r *RoomService) CreateRoom(ctx context.Context, hostID int) (*model.Room, error) {
	const errHeader = roomServiceErrHeader + "CreateRoom"

	// Create room object
	room := model.NewRoom(model.NewRoomParam{
		ID:       0,
		HostID:   hostID,
		PuzzleID: -1,
	})
	// update room setting
	puzzle, err := r.puzzleMgr.GetPuzzleByDifficulty(room.Setting.Difficulty)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}
	room.PuzzleID = puzzle.ID

	// push into repo
	room, err = r.roomRepo.Create(*room)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	playerState := model.NewPlayerState(model.PlayerStateCfg{
		PlayerID:    hostID,
		Status:      model.StatusWaiting,
		Progress:    0,
		GamingScore: 0,
		Location: model.PlayerLocation{
			RoomID: room.ID,
			Seat:   len(room.PlayersID),
		},
		PlayingPuzzleID: room.PuzzleID,
	})

	_, err = r.playerMgr.UpdatePlayerState(hostID, *playerState)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// Push Event RoomList Added
	r.roomSyncMgr.PublishRoomListUpdated(ctx, models.RoomListUpdatedRes{
		Room: room,
		Kind: models.RoomListUpdatedKindAdded,
	})

	r.roomSyncMgr.InitAll(ctx, room.ID)
	return room, nil
}

// JoinRoom make a player joins the room
func (r *RoomService) JoinRoom(ctx context.Context, roomID int, playerID int) (*model.Room, error) {
	const errHeader = roomServiceErrHeader + "JoinRoom"

	if !r.roomMgr.Exist(ctx, roomID) {
		return nil, errors.Wrap(codec.ErrRoom404.WithField("roomID", roomID), errHeader)
	}
	if !r.playerMgr.Exist(playerID) {
		return nil, errors.Wrap(codec.ErrPlayer404.WithField("playerID", playerID), errHeader)
	}

	// get room
	room, err := r.roomRepo.GetOne(roomID)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// Let Player leaves the original room, to avoid player in multi room
	err = r.roomMgr.HandleLeaveRoom(ctx, playerID)
	if err != nil {
		// !! Corner case
		// !! TODO: Try another way
		if codec.ErrPlayerState404.IsErrorEqual(err) {
			// ignore when playerState not found
			// usually occur when server restart
		} else if codec.ErrRoom404.IsErrorEqual(err) {
			// ignore when room not found
			// not a really big problem
		} else {
			return nil, errors.Wrap(err, errHeader)
		}
	}

	// push to repo
	err = r.roomRepo.PushPlayer(room, playerID)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// Check current room status
	var playerStatus model.Status
	if room.Status == model.RoomStatusGaming {
		playerStatus = model.StatusGaming
	} else {
		playerStatus = model.StatusWaiting
	}

	playerState := model.NewPlayerState(model.PlayerStateCfg{
		PlayerID:    playerID,
		Status:      playerStatus,
		Progress:    0,
		GamingScore: 0,
		Location: model.PlayerLocation{
			RoomID: room.ID,
			Seat:   len(room.PlayersID),
		},
		PlayingPuzzleID: room.PuzzleID,
	})

	_, err = r.playerMgr.UpdatePlayerState(playerID, *playerState)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// notify players that room update
	r.roomSyncMgr.PublishRoomToPlayers(ctx, roomID, *room)
	return room, nil
}

// LeaveRoom make a player leave the room
func (r *RoomService) LeaveRoom(ctx context.Context, playerID int) error {
	const errHeader = roomServiceErrHeader + "LeaveRoom"

	err := r.roomMgr.HandleLeaveRoom(ctx, playerID)

	return errors.Wrap(err, errHeader)
}

// UpdateSetting update Room Details
func (r *RoomService) UpdateSetting(ctx context.Context, input UpdateSettingInput) (*model.Room, error) {
	const errHeader = roomServiceErrHeader + "UpdateSetting"

	room, err := r.roomRepo.GetOne(input.RoomID)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	if input.PlayerID != room.HostID {
		return nil, errors.Wrap(codec.ErrNotHost.WithFields(codec.CodeFields{
			"roomID":   room.ID,
			"playerID": input.PlayerID,
		}), errHeader)
	}

	// Update Puzzle if difficulty has changed
	if input.RoomSetting.Difficulty != room.Setting.Difficulty {
		newPuzzle, err := r.puzzleMgr.GetPuzzleByDifficulty(input.RoomSetting.Difficulty)
		if err != nil {
			return nil, errors.Wrap(err, errHeader)
		}
		err = r.roomRepo.UpdatePuzzleID(room, newPuzzle.ID)
		if err != nil {
			return nil, errors.Wrap(err, errHeader)
		}
	}

	err = r.roomRepo.UpdateSetting(room, input.RoomSetting)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// Add State
	go func() {
		r.roomSyncMgr.PublishRoomToPlayers(ctx, room.ID, *room)
	}()

	return room, nil
}

// TogglePlayerReadyOrWait change player status to ready or wait
func (r *RoomService) TogglePlayerReadyOrWait(ctx context.Context, playerID int, ready bool) (bool, error) {
	const errHeader = roomServiceErrHeader + "TogglePlayerReadyOrWait"

	var status model.Status
	if ready {
		status = model.StatusReady
	} else {
		status = model.StatusWaiting
	}

	state, err := r.playerMgr.UpdateStatus(playerID, status)

	if err != nil {
		return false, errors.Wrap(err, errHeader)
	}

	// query room
	roomID := state.Location.RoomID

	go func() {
		r.roomSyncMgr.PublishPlayersStateToPlayers(ctx, roomID, *state)
	}()

	return true, errors.Wrap(err, errHeader)
}

// StartGame start game
func (r *RoomService) StartGame(ctx context.Context, playerID int) error {
	const errHeader = roomServiceErrHeader + "StartGame"

	// find room
	// get player state
	playerState, err := r.playerMgr.GetPlayerState(playerID)
	if err != nil {
		return errors.Wrap(err, errHeader)
	}

	// get room id
	roomID := playerState.Location.RoomID
	room, err := r.roomRepo.GetOne(roomID)
	if err != nil {
		return errors.Wrap(err, errHeader)
	}

	// Check host
	if playerID != room.HostID {
		return errors.Wrap(codec.ErrNotHost.WithFields(codec.CodeFields{
			"roomID":   room.ID,
			"playerID": playerID,
		}), errHeader)
	}

	// Update all player state
	{
		g, _ := errgroup.WithContext(ctx)
		for _, player := range room.PlayersID {
			g.Go(func(playerID int) func() error {
				return func() error {
					state, err := r.playerMgr.GetPlayerState(playerID)
					if err != nil {
						return err
					}
					// update state
					staten := model.PlayerState{
						PlayerID:        state.PlayerID,
						Status:          model.StatusGaming,
						Progress:        0,
						PlayingPuzzleID: room.PuzzleID,
						GamingScore:     0,
						Location:        state.Location,
					}
					_, err = r.playerMgr.UpdatePlayerState(playerID, staten)
					return err
				}
			}(player))
		}

		err = g.Wait()
		if err != nil {
			return errors.Wrap(err, errHeader)
		}
	}

	// Update Room Status
	r.roomRepo.UpdateRoomStatus(room, model.RoomStatusGaming)

	// Notify all players
	go func() {
		r.roomSyncMgr.PublishRoomToPlayers(ctx, room.ID, *room)
	}()
	return nil
}

// PushAnswer commit answer
func (r *RoomService) PushAnswer(ctx context.Context, playerID int, answer sudoku.GridType) (*sudoku.AnswerRes, error) {
	const errHeader = roomServiceErrHeader + "PushAnswer"

	// get player state
	playerState, err := r.playerMgr.GetPlayerState(playerID)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}
	// get room id
	roomID := playerState.Location.RoomID
	if !r.roomMgr.Exist(ctx, roomID) {
		return nil, errors.Wrap(codec.ErrRoom404, errHeader)
	}

	// check type
	res, err := r.puzzleMgr.Check(playerState.PlayingPuzzleID, answer)
	if err != nil {
		return nil, err
	}

	ansRes := sudoku.CalculateScore(*res)

	// update score
	playerState.GamingScore = ansRes.Score
	playerState.Progress = ansRes.Progress
	r.playerMgr.UpdatePlayerState(playerID, *playerState)
	go func() {
		// notify
		r.roomSyncMgr.PublishPlayersStateToPlayers(ctx, roomID, *playerState)
	}()

	return &ansRes, nil
}

// SubscribePlayerState sub playerState
func (r *RoomService) SubscribePlayerState(ctx context.Context, playerID int) (<-chan *model.PlayerState, error) {
	const errHeader = "RoomService.SubscribePlayerState"
	// get location of room
	playerState, err := r.playerMgr.GetPlayerState(playerID)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// get roomID
	roomID := playerState.Location.RoomID
	if !r.roomMgr.Exist(ctx, roomID) {
		return nil, errors.Wrap(codec.ErrRoom404, errHeader)
	}

	ch := r.roomSyncMgr.SubscribePlayersToPlayerState(ctx, roomID, playerID)
	return ch, nil
}

// SubscribeGameTime sub gametTime
func (r *RoomService) SubscribeGameTime(ctx context.Context, playerID int) (<-chan model.TimeDuration, error) {
	const errHeader = "RoomService.SubscribeGameTime"
	// get location of room
	playerState, err := r.playerMgr.GetPlayerState(playerID)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// get roomID
	roomID := playerState.Location.RoomID
	if !r.roomMgr.Exist(ctx, roomID) {
		return nil, errors.Wrap(codec.ErrRoom404, errHeader)
	}

	ch := r.roomSyncMgr.SubscribePlayersToGameTime(ctx, roomID, playerID)

	return ch, nil
}

// SubscribeRoom sub room
func (r *RoomService) SubscribeRoom(ctx context.Context, playerID int) (<-chan *model.Room, error) {
	const errHeader = "RoomService.SubscribeRoom"
	// get location of room
	playerState, err := r.playerMgr.GetPlayerState(playerID)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}

	// get roomID
	roomID := playerState.Location.RoomID
	if !r.roomMgr.Exist(ctx, roomID) {
		return nil, errors.Wrap(codec.ErrRoom404, errHeader)
	}

	ch := r.roomSyncMgr.SubscribePlayersToRoom(ctx, roomID, playerID)
	return ch, nil
}

// SubscribeRoomListUpdate subscribe when room added or deleted in roomList
func (r *RoomService) SubscribeRoomListUpdate(ctx context.Context, playerID int) (<-chan *models.RoomListUpdatedRes, error) {
	ch := r.roomSyncMgr.SubscribeRoomListUpdated(ctx, playerID)
	return ch, nil
}
